﻿/**
 * AbstractEmitter
 *
 * @author		Allen Chou
 * @version 	1.1.1 (last update: Aug 26 2008)
 * @link        http://cjcat.blogspot.com
 * @link		http://cjcat2266.deviantart.com
 */

/*
 * Copyright 2008 (C) Allen Chou (cjcat2266@gmail.com)
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

/*
	CHANGE LOG:
		
		1.1.1:
			- Added "loadParticle" method, in company with the new XParticle class for defining external-loadable .swf document classes.
			
		1.1.0:
			- Added "container" property.
			- Added "addChildMode" property, in company with the new AddChildMode class for storing constants.
 */

package idv.cjcat.display.particles {

	import flash.display.*;
	import flash.errors.IllegalOperationError;
	import flash.events.*;
	import flash.net.*;
	
	import idv.cjcat.*;
	import idv.cjcat.display.*;
	import idv.cjcat.geom.*;
	
	public class AbstractEmitter extends Sprite {
		
		protected var _container:DisplayObjectContainer;
		protected var _displayClass:Class;
		private   var _addChildMode:String = AddChildMode.TOP;
		
		private   var _stepInterval:Number = 1;
		
		public    var centerX:Number = 0;
		public    var centerY:Number = 0;
		public    var oriented:Boolean = false;
		public    var bubbleOriented:Boolean = false;
		public    var bubbleOrientationMultiplier:Number = 0.5;
		public    var orientationOffset:Number = 0;
		
		private   var timer:Number = 0;
		
		//emitter properties
		protected var _direction:Vector2 = new Vector2(0, -1);
		protected var _active:Boolean = false;
		public    var directionVar:Number = 30;
		public    var bubbleMotion:Boolean = false;
		protected var _bubbleAmplitude:Number = 50;
		public    var bubbleAmplitudeVar:Number = 5;
		protected var _bubbleFrequency:Number = 1.5;
		public    var bubbleFrequencyVar:Number = 0.5;
		protected var _normal:Vector2 = new Vector2(0, -1);
		protected var _showAuxShape:Boolean;
		protected var _auxShape:Shape;
		public    var auxShapeLineStyle:LineStyle = new LineStyle();
		
		//ParticleData array
		private   var _pdArray:Array = new Array();
		
		//spawning parameters
		protected var _rate:Number = 0.2;
		protected var _rateVar:Number = 0;
		protected var _life:Number = 50;
		protected var _lifeVar:Number = 0;
		
		protected var _burstQueue:Array = new Array();
		protected var _uniBurstQueue:Array = new Array();
		
		//motion parameters
		protected var _gravities:Array = new Array();
		protected var _deflectors:Array = new Array();
		protected var _velocityField:AbstractField = null;
		protected var _velocityFieldOverride:Boolean = false;
		public    var scale:Number = 1;
		public    var scaleVar:Number = 0.2;
		public    var speed:Number = 4;
		public    var speedVar:Number = 0;
		public    var bidirectional:Boolean = false;
		public    var initAngle:Number = 0;
		public    var initAngleVar:Number = 180;
		public    var initAlpha:Number = 1;
		public    var initAlphaVar:Number = 0;
		public    var spin:Number = 0;
		public    var spinVar:Number = 1;
		public    var finalAlpha:Number = 0;
		public    var finalAlphaVar:Number = 0;
		public    var finalScale:Number = 0.5;
		public    var finalScaleVar:Number = 0;
		public    var alphaDecayAt:Number = Number.MAX_VALUE;
		public    var alphaDecayAtVar:Number = 0;
		public    var scaleDecayAt:Number = Number.MAX_VALUE;
		public    var scaleDecayAtVar:Number = 0;
		
		//this is an abstract class and should not be instantiated.
		public function AbstractEmitter(displayClass:Class = null, container:DisplayObjectContainer = null) {
			if (displayClass == null) displayClass = Shape;
			this.displayClass = displayClass;
			this.container = container;
		}
		
		
		//public methods
		//------------------------------------------------------------------------------------------------
		
		//this is an abstract function and should be overridden.
		public function spawn():DisplayObject { return new Shape(); }
		
		public function burst(count:int = 15, duration:Number = 1):void {
			if (count <= 0) return;
			if (duration <= 0) return;
			_burstQueue.push( { count:count, duration:duration , life:duration});
		}
		
		public function start():void {
			_active = true;
		}
		
		public function stop():void {
			_active = false;
		}
		
		private var xpLoader:Loader = new Loader();
		public function loadParticle(path:String):void {
			xpLoader.contentLoaderInfo.removeEventListener(Event.COMPLETE, xpComplete);
			
			xpLoader = new Loader();
			xpLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, xpComplete);
			xpLoader.load(new URLRequest(path));
		}
		private function xpComplete(e:Event):void {
			xpLoader.contentLoaderInfo.removeEventListener(Event.COMPLETE, xpComplete);
			if (!(xpLoader.content is XParticle)) throw new Error("The external particle .swf source file must be a XParticle object.");
			
			this.displayClass = XParticle(xpLoader.content).particleClass;
		}
		
		private var i:int;
		private var j:int;
		private var k:Number;
		private var m:Number;
		private var d:DisplayObject;
		private var o:Object;
		private var vecObj:Object;
		private var tempPD:ParticleData;
		public function step(e:Event = null):void {
			
			timer += _stepInterval;
			
			//process burst queue
			for (i = _burstQueue.length - 1; i >= 0; i--) {
				k = _burstQueue[i].count / _burstQueue[i].duration;
				for (j = 0; j < Math.floor(k * _stepInterval) + (Math.random() < ((k * _stepInterval + 1) % 1))?1:0; j++) {
					addParticle(spawn());
				}
				if ((_burstQueue[i].life -= _stepInterval) <= 0) _burstQueue.splice(i, 1);
			}
			
			//process uniBurst queue
			//looping through
			for (i = _uniBurstQueue.length - 1; i >= 0; i--) {
				//find element eligible to be processed
				if (_uniBurstQueue[i].life <= 0) {
					for (m = 0; m < _uniBurstQueue[i].array.length; m++) {
						tempPD = new ParticleData();
						d = new displayClass();
						o = _uniBurstQueue[i].array[m];
						d.x = o.x;
						d.y = o.y;
						d.rotation = CJMath.uniformSample(initAngle, initAngleVar, false);
						//d.rotation = o.rotation;
						d.alpha = CJMath.uniformSample(initAlpha, initAlphaVar, false);
						d.scaleX = d.scaleY = CJMath.uniformSample(scale, scaleVar, false);
						
						tempPD.particle =  _container.addChildAt(d, getNewDepth());
						tempPD.life = CJMath.uniformSample(_life, _lifeVar, false);
						tempPD.initLife = CJMath.uniformSample(_life, _lifeVar);
						tempPD.center = { x:d.x, y:d.y };
						tempPD.velocity = { x:o.vx, y:o.vy };
						tempPD.bubbleAmplitude = CJMath.uniformSample(_bubbleAmplitude, bubbleAmplitudeVar, false);
						tempPD.bubbleFrequency = CJMath.uniformSample(_bubbleFrequency, bubbleFrequencyVar, false);
						tempPD.bubblePhase = Math.random() * Math.PI * 2;
						tempPD.spin = CJMath.uniformSample(spin, spinVar, false);
						tempPD.initAlpha = d.alpha
						tempPD.finalAlpha = CJMath.uniformSample(finalAlpha, finalAlphaVar, false);
						tempPD.initScale = d.scaleX;
						tempPD.finalScale = CJMath.uniformSample(finalScale, finalScaleVar, false);
						tempPD.alphaDecayAt = CJMath.uniformSample(alphaDecayAt, alphaDecayAtVar, false);
						tempPD.scaleDecayAt = CJMath.uniformSample(scaleDecayAt, scaleDecayAtVar, false);
						
						_pdArray.push(tempPD);
					}
					//remove element from queue
					_uniBurstQueue.splice(i, 1);
					continue;
				}
				_uniBurstQueue[i].life -= _stepInterval;
			}
			
			//process normal emission
			if (_active) {
				for (i = 0; i < Math.floor(rate * _stepInterval) + (Math.random() < ((rate * _stepInterval) % 1))?1:0; i++) {
					addParticle(spawn());
				}
			}
			
			//process motions
			for (i = _pdArray.length - 1; i >= 0; i--) {
				if ((_pdArray[i].life -= _stepInterval) <= 0) {
					DisplayObjectContainer(_pdArray[i].particle.parent).removeChild(_pdArray[i].particle);
					_pdArray.splice(i, 1);
					continue;
				}
				
				//process accelerations and velocities
				processMotion(i, vecObj);
				
				//check orientation
				if (!oriented) {
					_pdArray[i].particle.rotation += _pdArray[i].spin * _stepInterval;
				} else {
					if (!bubbleOriented) {
						_pdArray[i].particle.rotation = Math.atan2(_pdArray[i].velocity.y, _pdArray[i].velocity.x) / Math.PI * 180 + orientationOffset + 90;
					} else {
						_pdArray[i].particle.rotation = Math.atan2(_pdArray[i].velocity.y + bubbleVec.y * _pdArray[i].bubbleFrequency / 15 * bubbleOrientationMultiplier, _pdArray[i].velocity.x - bubbleVec.x * _pdArray[i].bubbleFrequency / 15 * bubbleOrientationMultiplier) / Math.PI * 180 + orientationOffset + 90;
					}
				}
				
				if (_pdArray[i].life < _pdArray[i].initLife - alphaDecayAt) {
					_pdArray[i].particle.alpha = CJMath.interpolate(1, _pdArray[i].initAlpha, 0, _pdArray[i].finalAlpha, _pdArray[i].life / (_pdArray[i].initLife - _pdArray[i].alphaDecayAt));
				}
				if (_pdArray[i].life < _pdArray[i].initLife - scaleDecayAt) {
					_pdArray[i].particle.scaleX =
					_pdArray[i].particle.scaleY = CJMath.interpolate(1, _pdArray[i].initScale, 0, _pdArray[i].finalScale, _pdArray[i].life / (_pdArray[i].initLife - _pdArray[i].scaleDecayAt));
				}
			}
			
			//process auxiliary shape
			if (_container == this) updateAuxShape();
			else graphics.clear();
		}
		
		public function addGravity(gravityField:AbstractField):void {
			CJUtils.addUniqueElement(_gravities, gravityField);
		}
		
		public function removeGravity(gravityField:AbstractField):void {
			CJUtils.removeUniqueElement(_gravities, gravityField);
		}
		
		public function clearGravities():void {
			_gravities = new Array();
		}
		
		public function addDeflector(deflector:AbstractDeflector):void {
			CJUtils.addUniqueElement(_deflectors, deflector);
		}
		
		public function removeDeflector(deflector:AbstractDeflector):void {
			CJUtils.removeUniqueElement(_deflectors, deflector);
		}
		
		public function clearDeflectors():void {
			_deflectors = new Array();
		}
		
		public function getConfig(mask:EmitterConfigMask = null):Object {
			if (mask == null) mask = new EmitterConfigMask();
			
			var obj:Object = new Object();
			if (mask.x) obj.x = this.x;
			if (mask.y) obj.y = this.y;
			if (mask.rotation) obj.rotation = this.rotation;
			if (mask.scaleX) obj.scaleX = this.scaleX;
			if (mask.scaleY) obj.scaleY = this.scaleY;
			
			if (mask.displayClass) obj.displayClass = this.displayClass;
			if (mask.container) obj.container = this.container;
			if (mask.addChildMode) obj.addChildMode = this.addChildMode;
			if (mask.centerX) obj.centerX = this.centerX;
			if (mask.centerY) obj.centerY = this.centerY;
			if (mask.oriented) obj.oriented = this.oriented;
			if (mask.orientationOffset) obj.orientationOffset = this.orientationOffset;
			if (mask.direction) obj.direction = this.direction;
			if (mask.directionVar) obj.directionVar = this.directionVar;
			if (mask.bubbleMotion) obj.bubbleMotion = this.bubbleMotion;
			if (mask.bubbleAmplitude) obj.bubbleAmplitude = this.bubbleAmplitude;
			if (mask.bubbleamplitudeVar) obj.bubbleAmplitudeVar = this.bubbleAmplitudeVar;
			if (mask.bubbleFrequency) obj.bubbleFrequency = this.bubbleFrequency;
			if (mask.bubbleFrequencyVar) obj.bubbleFrequencyVar = this.bubbleFrequencyVar;
			if (mask.normal) obj.normal = this.normal;
			if (mask.showAuxShape) obj.showAuxShape = this.showAuxShape;
			if (mask.auxShapeLineStyle) obj.auxShapeLineStyle = this.auxShapeLineStyle;
			if (mask.rate) obj.rate = this.rate;
			if (mask.rateVar) obj.rateVar = this.rateVar;
			if (mask.life) obj.life = this.life;
			if (mask.lifeVar) obj.lifeVar = this.lifeVar;
			
			if (mask.gravaties) obj.gravities = this._gravities;
			if (mask.deflectors) obj.deflectors = this._deflectors;
			if (mask.velocityField) obj.velocityField = this.velocityField;
			if (mask.velocityFieldOverride) obj.velocityFieldOverride = this.velocityFieldOverride;
			
			if (mask.scale) obj.scale = this.scale;
			if (mask.scaleVar) obj.scaleVar = this.scaleVar;
			if (mask.speed) obj.speed = this.speed;
			if (mask.speedVar) obj.speedVar = this.speedVar;
			if (mask.bidirectional) obj.bidirectional = this.bidirectional;
			if (mask.initAngle) obj.initAngle = this.initAngle;
			if (mask.initAngleVar) obj.initAngleVar = this.initAngleVar;
			if (mask.initAlpha) obj.initAlpha = this.initAlpha;
			if (mask.initAlphaVar) obj.initAlphaVar = this.initAlphaVar;
			if (mask.spin) obj.spin = this.spin;
			if (mask.spinVar) obj.spinVar = this.spinVar;
			if (mask.finalAlpha) obj.finalAlpha = this.finalAlpha;
			if (mask.finalAlphaVar) obj.finalAlphaVar = this.finalAlphaVar;
			if (mask.finalScale) obj.finalScale = this.finalScale;
			if (mask.finalScaleVar) obj.finalScaleVar = this.finalScaleVar;
			if (mask.alphaDecayAt) obj.alphaDecayAt = this.alphaDecayAt;
			if (mask.alphaDecayAtVar) obj.alphaDecayAtVar = this.alphaDecayAtVar;
			if (mask.scaleDecayAt) obj.scaleDecayAt = this.scaleDecayAt;
			if (mask.scaleDecayAtVar) obj.scaleDecayAtVar = this.scaleDecayAtVar;
			
			return obj;
		}
		
		public function setConfig(configObj:Object, mask:EmitterConfigMask = null):void {
			if (mask == null) mask = new EmitterConfigMask();
			for (var i:String in configObj) {
				if (i == "gravities") {
					var temp:Array = new Array();
					for (var j:int = 0; j < configObj.gravities.length; j++) {
						temp.push(configObj.gravities[j]);
					}
					this._gravities = temp;
					continue;
				} else if (i == "deflectors") {
					var temp2:Array = new Array();
					for (var k:int = 0; k < configObj.deflectors.length; k++) {
						temp2.push(configObj.deflectors[k]);
					}
					this._deflectors = temp2;
					continue;
				}
				
				try {
					if (mask[i] == true) this[i] = configObj[i];
				} catch (e:Error) {
					continue;
				}
			}
		}
		
		//end of public methods
		//------------------------------------------------------------------------------------------------
		
		
		//internal functions
		//------------------------------------------------------------------------------------------------
		
		private var getNewDepth:Function = gndTop;
		private function gndTop():int {	return _container.numChildren; }
		private function gndBottom():int { return 0; }
		private function gndRandom():int { return CJMath.randomInt(_container.numChildren); }
		
		private var bubbleVec:Vector2;
		private var processMotion:Function = processMotion_default;
		private function processMotion_default(i:Number, vecObj:Object):void {
			//process gravaties
			for (m = 0; m < _gravities.length; m++) {
				if (!_gravities[m].active) continue;
				vecObj = AbstractField(_gravities[m]).getVectorObj(_pdArray[i].particle.x, _pdArray[i].particle.y);
				_pdArray[i].velocity.x += vecObj.x;
				_pdArray[i].velocity.y += vecObj.y;
			}
			
			_pdArray[i].center.x += _pdArray[i].velocity.x;
			_pdArray[i].center.y += _pdArray[i].velocity.y;
			
			//process deflectors
			for (m = 0; m < _deflectors.length; m++) {
				if (!_deflectors[m].active) continue;
				vecObj = AbstractDeflector(_deflectors[m]).getVectorObj(_pdArray[i].center.x, _pdArray[i].center.y, _pdArray[i].velocity.x, _pdArray[i].velocity.y);
				_pdArray[i].velocity.x = vecObj.vx;
				_pdArray[i].velocity.y = vecObj.vy;
				if (vecObj.newX != undefined) _pdArray[i].center.x = vecObj.newX;
				if (vecObj.newY != undefined) _pdArray[i].center.y = vecObj.newY;
			}
			
			//process final position
			if (!bubbleMotion) {
				_pdArray[i].particle.x = _pdArray[i].center.x;
				_pdArray[i].particle.y = _pdArray[i].center.y;
			} else {
				bubbleVec = new Vector2(_pdArray[i].velocity.x, _pdArray[i].velocity.y);
				bubbleVec = bubbleVec.rotate(90, false);
				bubbleVec.length = _pdArray[i].bubbleAmplitude * Math.sin((_pdArray[i].bubblePhase + timer) * _pdArray[i].bubbleFrequency / 15);
				_pdArray[i].particle.x = _pdArray[i].center.x + bubbleVec.x;
				_pdArray[i].particle.y = _pdArray[i].center.y + bubbleVec.y;
			}
			
		}
		private function processMotion_overridden(i:Number, vecObj:Object):void {
			vecObj = velocityField.getVectorObj(_pdArray[i].particle.x, _pdArray[i].particle.y);
			_pdArray[i].velocity.x = vecObj.x;
			_pdArray[i].velocity.y = vecObj.y;
		}
		
		private var tempDirVec:Vector2;
		private function addParticle(obj:DisplayObject):void {
			tempDirVec = _direction.unitVec().rotate(CJMath.uniformSample(0, directionVar, false), false);
			if (bidirectional) {
				if (Math.random() >= 0.5) tempDirVec.x *= -1, tempDirVec.y *= -1;
			}
			
			obj.rotation = CJMath.uniformSample(initAngle, initAngleVar, false);
			obj.alpha = CJMath.uniformSample(initAlpha, initAlphaVar, false);
			obj.scaleX = obj.scaleY = CJMath.uniformSample(scale, scaleVar, false);
			
			tempPD = new ParticleData();
			
			tempPD.particle = _container.addChildAt(obj, getNewDepth());
			tempPD.life = CJMath.uniformSample(_life, _lifeVar, false);
			tempPD.initLife = CJMath.uniformSample(_life, _lifeVar);
			tempPD.center = { x:obj.x, y:obj.y };
			tempPD.velocity = { x:speed * tempDirVec.x, y:speed * tempDirVec.y };
			tempPD.bubbleAmplitude = CJMath.uniformSample(_bubbleAmplitude, bubbleAmplitudeVar, false);
			tempPD.bubbleFrequency = CJMath.uniformSample(_bubbleFrequency, bubbleFrequencyVar, false);
			tempPD.bubblePhase = Math.random() * Math.PI * 2;
			tempPD.spin = CJMath.uniformSample(spin, spinVar, false);
			tempPD.initAlpha = obj.alpha;
			tempPD.finalAlpha = CJMath.uniformSample(finalAlpha, finalAlphaVar, false);
			tempPD.initScale = obj.scaleX;
			tempPD.finalScale = CJMath.uniformSample(finalScale, finalScaleVar, false);
			tempPD.alphaDecayAt = CJMath.uniformSample(alphaDecayAt, alphaDecayAtVar, false);
			tempPD.scaleDecayAt = CJMath.uniformSample(scaleDecayAt, scaleDecayAtVar, false);
			
			_pdArray.push(tempPD);
		}
		
		//end of internal functions
		//------------------------------------------------------------------------------------------------
		
		
		//accessors
		//------------------------------------------------------------------------------------------------
		
		public function get stepInterval():Number { return _stepInterval; }
		public function set stepInterval(value:Number):void {
			_stepInterval = CJMath.clamp(value, 0.001, 1000);
		}
		
		public function get container():DisplayObjectContainer {
			if (_container != this) return _container;
			else return null;
		}
		public function set container(value:DisplayObjectContainer):void {
			if (value == null) {
				_container = this;
				return;
			} else {
				_container = value;
				return;
			}
		}
		
		public function get displayClass():Class { return _displayClass; }
		public function set displayClass(value:Class):void {
			var temp:DisplayObject = new value	();
			_displayClass = value;
		}
		
		public function get addChildMode():String { return _addChildMode; }
		public function set addChildMode(value:String):void {
			switch (value) {
				case AddChildMode.BOTTOM:
					_addChildMode = AddChildMode.BOTTOM;
					getNewDepth = gndBottom;
					break;
				case AddChildMode.RANDOM:
					_addChildMode = AddChildMode.RANDOM;
					getNewDepth = gndRandom;
					break;
				default:
					_addChildMode = AddChildMode.TOP;
					getNewDepth = gndTop;
					break;
			}
		}
		
		//this is an abstract function and should be overridden.
		public function updateAuxShape(e:Event = null):void { }
		
		public function get direction():Vector2 { return _direction; }
		public function set direction(value:Vector2):void {
			_direction.x = value.x;
			_direction.y = value.y;
		}
		
		public function get bubbleFrequency():Number { return _bubbleFrequency; }
		public function set bubbleFrequency(value:Number):void {
			_bubbleFrequency = CJMath.clamp(value, Number.MIN_VALUE, Number.MAX_VALUE);
		}
		
		public function get bubbleAmplitude():Number { return _bubbleAmplitude; }
		public function set bubbleAmplitude(value:Number):void {
			_bubbleAmplitude = CJMath.clamp(value, 0, Number.MAX_VALUE);
		}
		
		public function get normal():Vector2 { return _normal; }
		public function set normal(value:Vector2):void {
			_normal.x = value.x;
			_normal.y = value.y;
		}
		
		//this is an abstract function and should be overridden.
		public function get showAuxShape():Boolean { return _showAuxShape; }
		public function set showAuxShape(value:Boolean):void { }
		
		//spaning parameters
		public function get life():Number { return _life; }
		public function set life(value:Number):void {
			_life = value;
		}
		
		public function get lifeVar():Number { return _lifeVar; }
		public function set lifeVar(value:Number):void {
			_lifeVar = value;
		}
		
		public function get rate():Number { return _rate; }
		public function set rate(value:Number):void {
			_rate = value;
		}
		
		public function get rateVar():Number { return _rateVar; }
		public function set rateVar(value:Number):void {
			_rateVar = value;
		}
		
		public function get velocityField():AbstractField { return _velocityField; }
		public function set velocityField(value:AbstractField):void {
			_velocityField = value;
			velocityFieldOverride = velocityFieldOverride;
		}
		
		public function get velocityFieldOverride():Boolean { return _velocityFieldOverride; }
		public function set velocityFieldOverride(value:Boolean):void {
			_velocityFieldOverride = value;
			if (value && velocityField != null) {
				processMotion = processMotion_overridden;
			} else {
				processMotion = processMotion_default;
			}
		}
		
		//end of accessors
		//------------------------------------------------------------------------------------------------
		
	}
}